"""File operation utilities - secure file and directory management."""

import hashlib
import shutil
from pathlib import Path

from loguru import logger


class FileOperations:
    """A collection of static methods for safe and robust file and directory operations.

    This class provides utilities for common file system tasks such as ensuring
    directory existence, generating safe filenames, calculating and verifying
    file hashes, and safely moving, copying, and deleting files. It also
    includes methods for retrieving file sizes and checking disk space.
    """

    @staticmethod
    def ensure_directory(path: Path | str) -> Path:
        """Ensures that a directory exists, creating it if necessary.

        Args:
            path: The path to the directory, can be a string or a Path object.

        Returns:
            A Path object representing the ensured directory.
        """
        dir_path = Path(path)
        dir_path.mkdir(parents=True, exist_ok=True)
        logger.debug(f"确保目录存在: {dir_path}")
        return dir_path

    @staticmethod
    def safe_filename(filename: str, max_length: int = 255) -> str:
        """Generates a safe filename by removing unsafe characters and limiting length.

        Args:
            filename: The original filename string.
            max_length: The maximum allowed length for the safe filename.

        Returns:
            A string representing the safe filename.
        """
        # 移除不安全字符
        unsafe_chars = '<>:"/\\|?*'
        safe_name = "".join(c if c not in unsafe_chars else "_" for c in filename)

        # 移除首尾空格和点
        safe_name = safe_name.strip(". ")

        # 限制长度
        if len(safe_name) > max_length:
            name_part, ext_part = Path(safe_name).stem, Path(safe_name).suffix
            max_name_len = max_length - len(ext_part)
            safe_name = name_part[:max_name_len] + ext_part

        return safe_name or "unnamed"

    @staticmethod
    def get_unique_filename(filepath: Path) -> Path:
        """Generates a unique filename by appending a numeric suffix if the file already exists.

        Args:
            filepath: The original desired file path.

        Returns:
            A Path object representing a unique file path.
        """
        if not filepath.exists():
            return filepath

        stem = filepath.stem
        suffix = filepath.suffix
        parent = filepath.parent
        counter = 1

        while True:
            new_name = f"{stem}_{counter}{suffix}"
            new_path = parent / new_name
            if not new_path.exists():
                logger.debug(f"生成唯一文件名: {new_path}")
                return new_path
            counter += 1

    @staticmethod
    def calculate_hash(filepath: Path, algorithm: str = "md5", chunk_size: int = 8192) -> str:
        """Calculates the hash of a given file using the specified algorithm.

        Args:
            filepath: The path to the file.
            algorithm: The hashing algorithm to use (e.g., "md5", "sha1", "sha256").
            chunk_size: The size of chunks to read from the file.

        Returns:
            A string representing the hexadecimal digest of the file's hash.
        """
        hash_obj = hashlib.new(algorithm)

        with open(filepath, "rb") as f:
            while chunk := f.read(chunk_size):
                hash_obj.update(chunk)

        hash_value = hash_obj.hexdigest()
        logger.debug(f"计算文件哈希 {algorithm}: {hash_value}")
        return hash_value

    @staticmethod
    def verify_hash(filepath: Path, expected_hash: str, algorithm: str = "md5") -> bool:
        """Verifies the hash of a file against an expected hash value.

        Args:
            filepath: The path to the file.
            expected_hash: The expected hash value (case-insensitive).
            algorithm: The hashing algorithm used (e.g., "md5", "sha1", "sha256").

        Returns:
            True if the calculated hash matches the expected hash, False otherwise.
        """
        try:
            actual_hash = FileOperations.calculate_hash(filepath, algorithm)
            is_valid = actual_hash.lower() == expected_hash.lower()

            if is_valid:
                logger.info(f"文件哈希验证成功: {filepath}")
            else:
                logger.warning(
                    f"文件哈希不匹配: {filepath}\n" f"期望: {expected_hash}\n实际: {actual_hash}"
                )

            return is_valid
        except Exception as e:
            logger.error(f"哈希验证失败: {e}")
            return False

    @staticmethod
    def safe_move(src: Path, dst: Path, overwrite: bool = False) -> bool:
        """Safely moves a file from a source path to a destination path.

        If the destination file already exists and `overwrite` is False, a unique
        filename will be generated for the destination.

        Args:
            src: The source file path.
            dst: The destination file path.
            overwrite: If True, overwrite the destination file if it exists.

        Returns:
            True if the file was moved successfully, False otherwise.
        """
        try:
            if not src.exists():
                logger.error(f"源文件不存在: {src}")
                return False

            # 确保目标目录存在
            FileOperations.ensure_directory(dst.parent)

            # 处理目标文件已存在的情况
            if dst.exists() and not overwrite:
                dst = FileOperations.get_unique_filename(dst)

            shutil.move(str(src), str(dst))
            logger.info(f"文件已移动: {src} -> {dst}")
            return True

        except Exception as e:
            logger.error(f"移动文件失败: {e}")
            return False

    @staticmethod
    def safe_copy(src: Path, dst: Path, overwrite: bool = False) -> bool:
        """Safely copies a file from a source path to a destination path.

        If the destination file already exists and `overwrite` is False, a unique
        filename will be generated for the destination.

        Args:
            src: The source file path.
            dst: The destination file path.
            overwrite: If True, overwrite the destination file if it exists.

        Returns:
            True if the file was copied successfully, False otherwise.
        """
        try:
            if not src.exists():
                logger.error(f"源文件不存在: {src}")
                return False

            # 确保目标目录存在
            FileOperations.ensure_directory(dst.parent)

            # 处理目标文件已存在的情况
            if dst.exists() and not overwrite:
                dst = FileOperations.get_unique_filename(dst)

            shutil.copy2(str(src), str(dst))
            logger.info(f"文件已复制: {src} -> {dst}")
            return True

        except Exception as e:
            logger.error(f"复制文件失败: {e}")
            return False

    @staticmethod
    def safe_delete(filepath: Path, missing_ok: bool = True) -> bool:
        """Safely deletes a file or directory.

        Args:
            filepath: The path to the file or directory to delete.
            missing_ok: If True, no error is raised if the path does not exist.

        Returns:
            True if the file/directory was deleted or did not exist (and `missing_ok` is True),
            False otherwise.
        """
        try:
            if filepath.is_file():
                filepath.unlink()
                logger.info(f"文件已删除: {filepath}")
                return True
            elif filepath.is_dir():
                shutil.rmtree(filepath)
                logger.info(f"目录已删除: {filepath}")
                return True
            elif missing_ok:
                logger.debug(f"文件不存在（忽略）: {filepath}")
                return True
            else:
                logger.error(f"文件不存在: {filepath}")
                return False

        except Exception as e:
            logger.error(f"删除文件失败: {e}")
            return False

    @staticmethod
    def get_file_size(filepath: Path) -> int | None:
        """Retrieves the size of a file in bytes.

        Args:
            filepath: The path to the file.

        Returns:
            The size of the file in bytes if it exists, otherwise None.
        """
        try:
            if filepath.exists() and filepath.is_file():
                return filepath.stat().st_size
            return None
        except Exception as e:
            logger.error(f"获取文件大小失败: {e}")
            return None

    @staticmethod
    def format_size(size_bytes: int | None) -> str:
        """Formats a given size in bytes into a human-readable string (e.g., "1.23 MB").

        Args:
            size_bytes: The size in bytes, or None if unknown.

        Returns:
            A formatted string representing the size.
        """
        if size_bytes is None:
            return "未知"

        units = ["B", "KB", "MB", "GB", "TB", "PB"]
        size = float(size_bytes)
        unit_index = 0

        while size >= 1024 and unit_index < len(units) - 1:
            size /= 1024
            unit_index += 1

        return f"{size:.2f} {units[unit_index]}"

    @staticmethod
    def get_available_space(path: Path) -> int:
        """Retrieves the available disk space for a given path.

        Args:
            path: The path to check disk space for.

        Returns:
            The amount of free disk space in bytes.
        """
        try:
            stat = shutil.disk_usage(path)
            return stat.free
        except Exception as e:
            logger.error(f"获取磁盘空间失败: {e}")
            return 0

    @staticmethod
    def has_enough_space(path: Path, required_bytes: int, buffer_mb: int = 100) -> bool:
        """Checks if there is enough available disk space at a given path.

        Includes an optional buffer for safety.

        Args:
            path: The path to check disk space for.
            required_bytes: The amount of space required in bytes.
            buffer_mb: An additional buffer in megabytes to add to the required space.

        Returns:
            True if there is enough space, False otherwise.
        """
        available = FileOperations.get_available_space(path)
        buffer_bytes = buffer_mb * 1024 * 1024
        required_with_buffer = required_bytes + buffer_bytes

        if available < required_with_buffer:
            logger.warning(
                f"磁盘空间不足: "
                f"需要 {FileOperations.format_size(required_with_buffer)}, "
                f"可用 {FileOperations.format_size(available)}"
            )
            return False

        return True


# 便捷函数
def ensure_dir(path: Path | str) -> Path:
    """Convenience function to ensure a directory exists.

    Args:
        path: The path to the directory.

    Returns:
        A Path object representing the ensured directory.
    """
    return FileOperations.ensure_directory(path)


def safe_filename(filename: str) -> str:
    """Convenience function to generate a safe filename.

    Args:
        filename: The original filename string.

    Returns:
        A string representing the safe filename.
    """
    return FileOperations.safe_filename(filename)


def calculate_hash(filepath: Path, algorithm: str = "md5") -> str:
    """Convenience function to calculate the hash of a file.

    Args:
        filepath: The path to the file.
        algorithm: The hashing algorithm to use.

    Returns:
        A string representing the hexadecimal digest of the file's hash.
    """
    return FileOperations.calculate_hash(filepath, algorithm)


def verify_hash(filepath: Path, expected_hash: str, algorithm: str = "md5") -> bool:
    """Convenience function to verify the hash of a file.

    Args:
        filepath: The path to the file.
        expected_hash: The expected hash value.
        algorithm: The hashing algorithm used.

    Returns:
        True if the calculated hash matches the expected hash, False otherwise.
    """
    return FileOperations.verify_hash(filepath, expected_hash, algorithm)
